--[[
                          =============================
                          AvgXP Soulstoned by Bratta 
                         =============================

===============================================================================   

    This file is part of AvgXP Soulstoned.

    AvgXP Soulstoned is free software; you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation; either version 2 of the License, or
    (at your option) any later version.

    AvgXP Soulstoned is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with AvgXP Soulstoned; if not, write to the Free Software
    Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA

    ---

    This mod for World of Warcraft 2.0+ and The Burning Crusade expansion is 
    a fork of the AvgXP Deluxe project by Norbet for World of Warcraft up to 
    patch 1.12.  AvgXP Deluxe is a mod of the Average Exp Calculator 
    originally by jINx of Purgatory LAN Organization. AvgXP Plus by Tolin 
    added the XP Progress bar in the background, stretched out the 
    bar a little and added the current xp/total xp for the lvl. Nobula 
    changed the dimensions and the Z axis of the UI so it would fit on top 
    of the default XP bar and added 'Hide' and 'Show' functionality, 'Move' 
    and 'Lock' functionality and the 'Time to Level' features.

    Free to Modify and use, please retain at least this portion of the remarks 
    so that Bratta, Norbet, Azgaurd, Nobula, JiNx, and Tolin receive credit 
    for our work.

    Currently this is merely an average exp gain. Often times the mobile count 
    is not entirely accurate, as its based on if you gain the average exp. 
    Though if you let it go across your entire level until it resets it will 
    give you information regarding your average exp gain across that entire 
    level per kill. It stores the average exp per level that you gain.

===============================================================================   
]]--


-----------------------------------------------------------------------------------------
-- Vars
-----------------------------------------------------------------------------------------
jx_avgexp_version       = 2.2;
jx_last_xp_gained 	    = 0
jx_avg_xp_per_mob 	    = 0
jx_num_mobs_left        = 0
jx_total_xp_gained	    = 0
jx_total_mobs_killed	= 0
jx_kill_history         = { }
jx_kill_history_dep     = { }
jx_exp_history          = { }

is_loaded               = false;
is_saved                = false;
jx_oldversion           = false;


_ui_loaded              = false
_xp_history             = {}
_moving_avg_span        = 30
_last_ui_update         = 0
_update_delay           = 2
DEBUG                   = false

BarCR                   = 0;
BarCG                   = .4;
BarCB                   = 1;

-- Constants
jx_alliedraces          = { AVGXP_DWARF, AVGXP_GNOME, AVGXP_HUMAN, AVGXP_NIGHT_ELF, AVGXP_DRAENEI };
ui_text                 = "testing";

-- Level to hide the XP bar (updated for the Burning Crusade cap raise)
jx_levelcap             = 70;


-----------------------------
-- AvgXP Calculator Functions
-- --------------------------
--
local function display(output)
	local anyoutput = false;

	-- IF the output ends without a carriage return but has some in it, the entire message will not display
	for msg in string.gmatch(output,"(.+)\n") do
		if (msg~=nil) then
			anyoutput = true;
		end
		DEFAULT_CHAT_FRAME:AddMessage(msg);
	end

	-- This basically means that there were no carriage returns
	if (not anyoutput) then
		DEFAULT_CHAT_FRAME:AddMessage(output);
	end
end

-- This sets the width of the proper xp bar. Kinda important
-- Only going to do this once the UI has fully loaded.
function AvgXP_Progress_Update()
    if (is_loaded) then
        local CXP = UnitXP(AVGXP_PLAYER);
        local NXP = UnitXPMax(AVGXP_PLAYER);

		--set the width of the bars
		AvgXP:SetWidth(AvgXPWidth);
		EXPBar:SetWidth(AvgXPWidth-6);
		EXPBarb:SetWidth(AvgXPWidth-6);
		--set the height of the bars
		AvgXP:SetHeight(AvgXPHeight);
		EXPBar:SetHeight(AvgXPHeight-8);
		EXPBarb:SetHeight(AvgXPHeight-8);
		--set the alpha of the bars
		AvgXP:SetAlpha(AvgXPAlphaAll);
		EXPBar:SetAlpha(AvgXPAlpha);

		--set the value of the bars
        EXPBar:SetMinMaxValues(min(0, CXP), NXP);
        EXPBar:SetValue(CXP);
        EXPBarb:SetValue(NXP);
    end
end

-- determine the xp bar's color
function EXP_Bar_Color()
    jx_rested_bonus = GetXPExhaustion();
	if (jx_rested_bonus == nil) then
		BarCR = .8;
		BarCG = 0;
		BarCB = 1;
	else
		BarCR = 0;
		BarCG = .4;
		BarCB = 1;
	end
end


-- 'Move' and 'Lock' Functions
-- ---------------------------
--

function AvgXP_ToggleLock()
	if(AvgXPLocked) then
		AvgXP_Unlock();
	else
		AvgXP_Lock();
	end
end

function AvgXP_Lock()
	AvgXPLocked = true;
end

function AvgXP_Unlock()
	AvgXPLocked = false;
end

function AvgXP_StartMoving()
	if(not AvgXPLocked) then
		AvgXP:StartMoving();
	else
		display("\n"..AVGXP_LOCKED_MESSAGE.."\n");
	end
end


--
-- AvgXPRate Functions
-- -------------------
--
-- debug(msg)
--    msg -  a text message to add to the default chat frame
local function debug (msg)
   if not DEBUG then
      return
   end
   DEFAULT_CHAT_FRAME:AddMessage(AVGXP_DEBUG_MESSAGE .. msg .. "\n")
end

-- AvgXPRate_SecondsToTime(seconds)
--    seconds - number of seconds to use in conversion
--
-- converts seconds to an H:MM::SS text string
local function AvgXPRate_SecondsToTime(seconds)

   if not seconds or seconds < 0 then
      return string.format('-%s--%s--', AVGXP_TIME_SEP, AVGXP_TIME_SEP)
   end

   local s = math.fmod( seconds, 60 )
   local m = math.floor( math.fmod( seconds, 3600 ) / 60 )
   local h = math.floor( seconds / 3600 )

   return string.format('%d%s%02d%s%02d', h, AVGXP_TIME_SEP, m, AVGXP_TIME_SEP, s)

end

-- AvgXPRate_GetAvgXPRate()
--
-- calculates and returns the XP gain rate in XP/second
local function AvgXPRate_GetAvgXPRate()
   if table.getn(_xp_history) <= 1 then
      -- can't deduce a rate without 2 or more data points
      return
   end
   local start   = _xp_history[1].time
   local elapsed = GetTime() - start
   if elapsed < 1 then
      -- don't report infinite rates
      return
   end
   local xp_earned = 0
   for i,j in pairs(_xp_history) do
      xp_earned = xp_earned + j.xp_change
   end
   return xp_earned / elapsed
end

-- AvgXPRate_GetTimeToLevel(xp_rate)
--    xp_rate - the rate of XP gain in XP/seconds
--
-- calculates and returns the time needed to level
local function AvgXPRate_GetTimeToLevel(xp_rate)
   local xp_now   = UnitXP(AVGXP_PLAYER)
   local xp_level = UnitXPMax(AVGXP_PLAYER)
   local xp_todo  = xp_level - xp_now
   return AvgXPRate_SecondsToTime(math.floor(xp_todo / xp_rate))
end

-- AvgXPRate_GetXPToLevel(xp_rate)
--    togoXP - the rate of XP gain in XP/seconds
--
-- calculates and returns the time needed to level
local function AvgXPRate_GetXPToLevel()
   local currXP = UnitXP(AVGXP_PLAYER)
   local nextXP = UnitXPMax(AVGXP_PLAYER)

   local togoXP = nextXP - currXP
   if (togoXP < 0) then
     togoXP = 0
   end

   return togoXP
end

function AvgXPRate_UpdateUI()
   togoXP = AvgXPRate_GetXPToLevel()
   ui_text = string.format('%s0.0     %s-%s--%s--', AVGXP_XP_RATE, AVGXP_LEVEL_IN, AVGXP_TIME_SEP, AVGXP_TIME_SEP)
   xp_rate = AvgXPRate_GetAvgXPRate()
   if xp_rate then
      time_string = AvgXPRate_GetTimeToLevel(xp_rate)
      xp_per_min = string.format('%.1f', xp_rate * 60)
      ui_text = string.format('%s%d     %s%s', AVGXP_XP_RATE, xp_per_min, AVGXP_LEVEL_IN, time_string)
   end
   _last_ui_update = GetTime()
end

-- AvgXPRate_AddXPEntry(xp_change)
--    xp_change - the change in XP points
--
-- adds an XP change entry (XP amount and when it occurred) to history
local function AvgXPRate_AddXPEntry(xp_change)
   table.insert(_xp_history, { time = GetTime(), xp_change = xp_change } )
end

-- AvgXPRate_RemoveStale()
--
-- removes data from the XP history table if it has expired out of the
-- moving average window
local function AvgXPRate_RemoveStale()
   local earliest = GetTime() - _moving_avg_span * 60
   while _xp_history[1] and _xp_history[1].time < earliest do
      table.remove(_xp_history, 1)
   end
   -- reduce the spike in early rates of experience by zero-ing out
   -- the first experience event
   if _xp_history[1] then
      _xp_history[1].xp_change = 0
   end
end

-- AvgXPRate_OnLevelUp()
--
-- handler for PLAYER_LEVEL_UP events
local function AvgXPRate_OnLevelUp()
   debug("Level Up")
   _xp_history = {}
   AvgXPRate_UpdateUI()
end

-- AvgXPRate_OnXPUpdate()
--
-- handler for PLAYER_XP_UPDATE events
function AvgXPRate_OnXPUpdate()

   -- get the current XP amount
   local xp = UnitXP(AVGXP_PLAYER)
   -- if _xp_history is empty, this update is due to entering the game
   if (_ui_loaded) then
      debug("UI Load XP update");

      -- record the current xp amount
      _xp_current = xp
      _ui_loaded = false

   -- regular xp update
   else
      local xp_change = xp - _xp_current
      _xp_current = xp

      debug("Regular XP Change: " .. xp_change)

      AvgXPRate_AddXPEntry(xp_change)
      AvgXPRate_RemoveStale()

   end

   AvgXPRate_UpdateUI()

end

-----------------------------------------------------------------------------------------
-- Development

local DEBUG = false;
local jx_debug_threshold = 2;
local function debug(level,msg)
	if DEBUG then
		if (level >= jx_debug_threshold) then
			msg = YEL..AVGXP_DEBUG_MESSAGE..msg.."\n";
			display(msg);
		end
	end
end

-----------------------------------------------------------------------------------------
-- History Functions

local function JX_AvgXP_Deprecate_History()
	for i,v in pairs(jx_kill_history) do
		jx_kill_history_dep[ i ] = { };
		jx_kill_history_dep[ i ] = v;
	end

	jx_kill_history = { };
end

local function JX_AvgXP_AddKill(mobile_name,xp)
	local exp = 0
	local kills = 0
	local target = mobile_name;

	if (not jx_kill_history[ target ] ) then
		debug(2,string.format("Created kill record for %s", mobile_name));
		jx_kill_history[ target ] = { };
		jx_kill_history[ target ] = { level = UnitLevel(AVGXP_PLAYER), total_xp = 0, total_kills = 0 };
	end

	if (xp > 0) then
		exp = jx_kill_history[ target ].total_xp;
		kills = jx_kill_history[ target ].total_kills;
		jx_kill_history[ target ].total_xp = exp + xp;
		jx_kill_history[ target ].total_kills = kills + 1;
		jx_kill_history[ target ].level = UnitLevel(AVGXP_PLAYER);
		avg = (jx_kill_history[ target ].total_xp / jx_kill_history[ target ].total_kills);

		-- If we have a new best average, update it
		if (avg > jx_best_average) then
			jx_best_average = avg;
			jx_best_average_str = target;
		end

		debug(2,string.format("Adding kill for %s for %d XP, total XP now %d and kills %d :: Current average %1.f",mobile_name, xp, jx_kill_history[ target ].total_xp , jx_kill_history[ target ].total_kills, avg));
	else
		debug(2,string.format("No XP awarded for kill for %s", mobile_name));
	end
end

local function JX_AvgXP_Kill_Reset()
	jx_kill_history = { };
	display(YEL..AVGXP_KILLS_RESET);
end

local function JX_AvgXP_FindBestAvg()
	jx_best_average = 0
	jx_best_average_str = AVGXP_DEBUG_NA;
	local avg

	if (not jx_kill_history) then
		debug(2,"No kill history available for Best Average");
		return;
	end

	for k,v in pairs(jx_kill_history) do
		avg = (v.total_xp / v.total_kills);
		if( (avg > jx_best_average) and (v.total_kills > 1) ) then
			jx_best_average = avg;
			jx_best_average_str = k;
		end
	end
	debug(1,string.format("Set Best Average to %s at %.1f",jx_best_average_str,jx_best_average));
end

-----------------------------------------------------------------------------------------
-- Utility

local function JX_AvgXP_MobXP(plevel, mlevel)
	return (plevel * 5) + 45 + ( ( mlevel - plevel) * 17);
end

function JX_SetCommLang()
	for i, nextRace in ipairs(jx_alliedraces) do
		if (string.find(UnitRace(AVGXP_PLAYER),nextRace)~=nil) then
			jx_commlang = AVGXP_COMMON;
		end
	end
	if (not jx_commlang) then
		jx_commlang = AVGXP_ORCISH;
	end
end

function JX_AvgXP_SetTooltip()
	GameTooltip:SetOwner(this, "ANCHOR_CURSOR");
		if ( GetCVar("UberTooltips") == "1" ) then
			GameTooltip_SetDefaultAnchor(GameTooltip, this);
		else
			GameTooltip:SetOwner(this, "ANCHOR_CURSOR");
		end
	GameTooltip:ClearLines();

	if (not jx_target_stats) then
		GameTooltip:AddLine(AVGXP_AVGXP);
		GameTooltip:AddLine(ui_text,1,1,1);

		if (not jx_rested_bonus) then
			jx_rested_bonus = 0;
		end

		if (jx_rested_bonus > 0) then
			--figure out how much rested bonus per kill we got.
			local jx_new_last_xp_gained = 0;
			if (jx_rested_bonus >= jx_last_xp_gained) then
				jx_new_last_xp_gained = jx_last_xp_gained * 2;
			else
				jx_new_last_xp_gained = jx_last_xp_gained + jx_rested_bonus;
			end
			msg = string.format(AVGXP_TOTAL_RESTED, jx_total_mobs_killed, (jx_new_last_xp_gained),jx_rested_bonus);
		else
			msg = string.format(AVGXP_TOTAL, jx_total_mobs_killed, jx_last_xp_gained);
		end
		GameTooltip:AddLine(msg,1,1,1);

		if( (jx_last_xp_gained < jx_avg_xp_per_mob) and (jx_last_xp_gained ~= 0) ) then
			msg = AVGXP_LESS;
		else
			if (jx_last_xp_gained > jx_avg_xp_per_mob) then
				msg = AVGXP_MORE;
			else
				msg = "";
			end
		end
		GameTooltip:AddLine(msg,1,1,1);

		GameTooltip:AddLine(AVGXP_BEST,1,0.82,0);

		msg = string.format(AVGXP_XP_PER_KILL, jx_best_average_str, jx_best_average);
		GameTooltip:AddLine(msg,1,1,1);
	else
		if ( (jx_kill_history_dep[ UnitName(AVGXP_TARGET) ] ~= nil) and (not jx_kill_history[ UnitName(AVGXP_TARGET) ]) ) then
			-- Deprecated Target Stats
			GameTooltip:AddLine(UnitName(AVGXP_TARGET));

			local avg = (jx_kill_history_dep[ UnitName(AVGXP_TARGET) ].total_xp / jx_kill_history_dep[ UnitName(AVGXP_TARGET) ].total_kills);

			local approx_avg = avg - ( (UnitLevel(AVGXP_PLAYER) - jx_kill_history_dep[ UnitName(AVGXP_TARGET) ].level) * 17);

			GameTooltip:AddLine(string.format(AVGXP_LEVEL_AVG,jx_kill_history_dep[ UnitName(AVGXP_TARGET) ].level,avg),1,1,1);
			GameTooltip:AddLine(string.format(AVGXP_APPROX_AVG,approx_avg),1,1,1);
		else
			-- Target Stats
			GameTooltip:AddLine(UnitName(AVGXP_TARGET));
            local avg;
            local kills;
            
            if (jx_kill_history[ UnitName(AVGXP_TARGET) ] ~= nil) then
			    avg = (jx_kill_history[ UnitName(AVGXP_TARGET) ].total_xp / jx_kill_history[ UnitName(AVGXP_TARGET) ].total_kills);
			    kills = math.ceil( jx_exp_togo / avg );
            else
                avg = 0;
                kills = 0;
            end
			local kills_str = AVGXP_KILLS;

			if (kills == 1) then
				kills_str = AVGXP_KILL;
			end

			GameTooltip:AddLine(string.format(AVGXP_TAKE_TO_LEVEL, kills, kills_str),1,1,1);

			if (kills < jx_num_mobs_left) then
				GameTooltip:AddLine(AVGXP_FASTER,1,1,1);
			else
				if (kills == jx_num_mobs_left) then
					GameTooltip:AddLine(AVGXP_SAME,1,1,1);
				else
					GameTooltip:AddLine(AVGXP_SLOWER,1,1,1);
				end
			end

			if (UnitName(AVGXP_TARGET) == jx_best_average_str) then
				GameTooltip:AddLine(AVGXP_BEST_CURRENT,1,0.82,0);
			else
				msg = string.format(AVGXP_XP_LESS_THAN,(jx_best_average - avg), jx_best_average_str);
				GameTooltip:AddLine(msg,1,1,1);
			end
		end
	end

	GameTooltip:Show();
end

-----------------------------------------------------------------------------------------
-- Utility

-- Horrible way to comma delimit a number (this assumes its < 7 digits)
local function comma(val)
	local temp = tostring(val);
	if(string.len(temp) > 3) then
		return string.sub(temp,1,(string.len(temp) - 3))..","..string.sub(temp,(string.len(temp) - 3 + 1));
	else
		return temp;
	end
end

-- JX_AvgXP_Reset()
-- no parms
-- desc: zeros out variables
local function JX_AvgXP_Reset()
	jx_total_xp_gained = 0;
	jx_last_xp_gained = 0;
	jx_total_mobs_killed = 0;
	jx_avg_xp_per_mob = 0;
	jx_num_mobs_left = 0;
	_xp_history = {}
	AvgXPRate_UpdateUI();
end

-----------------------------------------------------------------------------------------
-- Level Up functions

local function JX_Level_Display(i,val)
	if (val~=nil) then
		msg = string.format(AVGXP_LEVEL_DISPLAY.."\n",i,val.avgxp,val.kills);
		display(WHT..msg);
	end
end

local function JX_AvgXP_Level_Report()
	msg = "none";
	display(YEL..AVGXP_GAIN_PER_LEVEL.."\n");
    -- In Lua 5.1, table.foreach and table.foreachi are deprecated
	--table.foreachi(jx_exp_history, JX_Level_Display);
    for idx,val in ipairs(jx_exp_history) do JX_Level_Display(idx,val) end 
	if (msg=="none") then
		display(WHT..AVGXP_NO_RECORDS.."\n");
	end
end

local function JX_AvgXP_Level_Reset()
	jx_exp_history = { };
	display(YEL..AVGXP_GAIN_RESET.."\n");
end

-- JX_Event_Player_Level_UP()
-- no parms
-- desc: Player Level Up, reset stats so we get Average Exp Gain across the entire level
function JX_Event_Player_Level_Up()
	-- Insert pair (level,average exp) into Exp History table
	table.insert(jx_exp_history, UnitLevel(AVGXP_PLAYER), { avgxp = jx_avg_xp_per_mob, kills = jx_total_mobs_killed } )

	JX_AvgXP_Level_Report();

	-- Report statistics
	msg = string.format(AVGXP_REPORT.."\n",YEL,UnitLevel(AVGXP_PLAYER),jx_avg_xp_per_mob);
	display(msg);

	JX_AvgXP_Deprecate_History();

	-- Reset average values for new level
	JX_AvgXP_Reset();
	display(YEL..AVGXP_RESET.."\n");
end



-----------------------------------------------------------------------------------------
-- Core functions

function JX_AvgXP_FindPID()
	--Wait till VARIABLES_LOADED has fired
	if( (UnitName(AVGXP_PLAYER)==nil) or (UnitName(AVGXP_PLAYER) == AVGXP_UNKNOWN) or (UnitName(AVGXP_PLAYER) == AVGXP_UNKNOWN_BEING) or (not GetCVar("realmName")) ) then
		return;
	end

	if (jx_avgxp_players == nil) then
		jx_avgxp_players = { };
		pid = -1;
		return;
	end

	--If the table is empty why even recurse, no PID exists
	if( (table.getn(jx_avgxp_players) == 0) or (table.getn(jx_avgxp_players)==nil) ) then
		-- If PID == Nil when it enters AvgXP_OnEvent it will fail
		-- We set to -1 to state that no pid currently exists, and when it enters Calc it will initialize and then save after first calc
		pid = -1;
		return;
	end

	if( (pid==nil) or (pid==-1) )then
		pid = -1;
		debug(1,string.format("Looking for PID...%d\n",table.getn(jx_avgxp_players)));
		for i,v in ipairs(jx_avgxp_players) do
			if( (v.race == nil) and (v.realm == nil) and (v.name == UnitName(AVGXP_PLAYER)) ) then
				-- Using version 0.32 to 0.36
				jx_oldversion = true;
				pid = i;
			end

			-- Check Name, Race, and Realm
			if( (v.name == UnitName(AVGXP_PLAYER)) and (v.race == UnitRace(AVGXP_PLAYER)) and (v.realm == GetCVar("realmName")) ) then
				pid = i;
			end
		end
	end
	if (pid ~= -1) then
		debug(1,string.format("PID is %d\n",pid));
	else
		debug(1,"No PID found\n");
	end
end

function JX_AvgXP_LoadPlayer()
	-- This calls after the player enters the world, this is necessary to detect UnitRace
	if (not jx_commlang) then
		JX_SetCommLang();
	end

	JX_AvgXP_FindPID();

	if (not AvgXPWidth) then
		AvgXPWidth = "1028";
	end

	if (not AvgXPHeight) then
		AvgXPHeight = "16";
	end

	if (not AvgXPAlphaAll) then
		AvgXPAlphaAll = "1";
	end

	if (not AvgXPAlpha) then
		AvgXPAlpha = "1";
	end

	if( (pid ~= -1) and (pid ~= nil) ) then
		jx_avgexp_init = jx_avgxp_players[pid].init;
		jx_total_mobs_killed = jx_avgxp_players[pid].totalmobs;
		jx_total_xp_gained = jx_avgxp_players[pid].totalxp;
		if (jx_oldversion) then
			jx_exp_history = { };
			jx_kill_history = { };
			jx_kill_history_dep = { };
			display(GRN..AVGXP_OLD_RESET.."\n");
		else
			jx_exp_history = jx_avgxp_players[pid].exphistory;
			jx_kill_history = jx_avgxp_players[pid].killhistory;
			if (not jx_kill_history) then
				jx_kill_history = { };
			end
			jx_kill_history_dep = jx_avgxp_players[pid].depkillhistory;
			if (not jx_kill_history_dep) then
				jx_kill_history_dep = { };
			end
		end
	else
		jx_avgexp_init = true;
		jx_total_mobs_killed = 0;
		jx_total_xp_gained = 0;
		jx_exp_history = { };
		jx_kill_history = { };
		jx_kill_history_dep = { };
	end

	jx_avg_xp_per_mob = 0;
	jx_num_mobs_left = 0;
	jx_last_xp_gained = 0;

	debug(1,"Load fired.\n");
end

function JX_AvgXP_SavePlayer()
	-- Locate Player ID
	JX_AvgXP_FindPID();

	-- If No Player ID insert first values
	if (pid == -1) then
		local stats = {
			name = UnitName(AVGXP_PLAYER),
			race = UnitRace(AVGXP_PLAYER),
			realm = GetCVar("realmName"),
			init = jx_avgexp_init,
			totalmobs = jx_total_mobs_killed,
			totalxp = jx_total_xp_gained,
			exphistory = jx_exp_history,
			killhistory = jx_kill_history,
			depkillhistory = jx_kill_history_dep };
		table.insert(jx_avgxp_players,stats);
	else
		-- Player existed overwrite old values
		jx_avgxp_players[pid].totalmobs = jx_total_mobs_killed;
		jx_avgxp_players[pid].totalxp = jx_total_xp_gained;
		jx_avgxp_players[pid].exphistory = jx_exp_history;
		jx_avgxp_players[pid].killhistory = jx_kill_history;
		jx_avgxp_players[pid].depkillhistory = jx_kill_history_dep;

		if (jx_oldversion) then
			jx_avgxp_players[pid].race = UnitRace(AVGXP_PLAYER);
			jx_avgxp_players[pid].realm = GetCVar("realmName");
		end
	end

	is_saved = true;

	debug(1,"Save fired.\n");
end



function AvgXP_OnLoad()

	-- Run Calculation and update UI (This will Save the Player at the end)
	-- AvgXP_OnEvent("JX_AVGXP_INIT");

	-- Calculate when Exp Gain. From Combat, XP Update (Quest / Exploration), Level Up
	this:RegisterEvent("PLAYER_XP_UPDATE");
	this:RegisterEvent("CHAT_MSG_COMBAT_XP_GAIN");
	this:RegisterEvent("PLAYER_LEVEL_UP");
	this:RegisterEvent("PLAYER_ENTERING_WORLD");
	this:RegisterEvent("PLAYER_TARGET_CHANGED");
	this:RegisterForDrag("LeftButton");

end

-- JX_AvgXP_Initialize()
-- No Parms
-- Desc: Initializes all values, runs OnLoad
function JX_AvgXP_Initialize()

	if(pid~=nil) then
		-- Check Player specific Init, if its false set default values
		if( (jx_avgexp_init==nil) or (jx_avgexp_init==false) ) then
			jx_total_mobs_killed = 0;
			jx_total_xp_gained = 0;
			jx_exp_history = { };
			jx_avgexp_init = true;
			JX_AvgXP_Reset();
			display(YEL..AVGXP_LOAD_RESET.."\n");
		end
	end

	-- Check Global init other initialize player table
	if( (jx_avgxp_globalinit==nil) or (jx_avgxp_globalinit==false) ) then
		jx_avgxp_players = { };
		jx_avgxp_globalinit = true;
	end
end



-- JX_AvgXP_Command(msg)
-- msg : Command line parameter
-- Desc: Parses the /avgxp command
function JX_AvgXP_Command(msg)
	-- Vars: removed all duplicate calculations, since nothing SHOULD change
	local output = "";

	if (not is_loaded) then
		return;
	end

-- Nanny
-- ToDo: Verify that "COMMON" in SendChatMessage works for Alliance and Horde

	-- No parameter
	if( (msg==nil) or (msg=="") or (msg=="help") or (msg=="?") ) then
		display(AVGXP_HELP_TEXT);
	end

	if( (UnitName(AVGXP_TARGET)~=nil) and (jx_target_stats~=nil) ) then
		output = jx_target_stats;
	else
		output = jx_avgexp_stats;
	end

	msg=string.lower(msg);

	-- If msg converts to a number cleanly, assume its a channel id
	-- ToDo: Something with Cosmos breaks SendChatMessage() to a channel. Research?
	if( (tonumber(msg)~=nil) ) then
		SendChatMessage(output,"CHANNEL",jx_commlang,tonumber(msg));
	end

	-- Reset parameter, clear stats
	if(msg=="reset") then
		JX_AvgXP_Reset();
		msg=YEL..AVGXP_LOAD_RESET.."\n";
		display(msg);

		-- Recalc and Update UI
		AvgXP_OnEvent("JX_AVGXP_INIT");
	end

	-- Reset Level parameter, clear exp history
	if(msg=="levelreset") then
		JX_AvgXP_Level_Reset();
	end

	-- Reset Kill History
	if(msg=="killreset") then
		JX_AvgXP_Kill_Reset();
	end

	-- Display Exp per Level Report
	if( (msg=="level") or (msg=="l") ) then
		JX_AvgXP_Level_Report();
	end

	-- Broadcast stats to party
	if( (msg=="party") or (msg=="p") ) then
		-- Verify they are in a party, yes it is redundancy checking
		if ( (GetNumPartyMembers()==nil) or (GetNumPartyMembers() < 1) ) then
			display(AVGXP_ERR_PARTY);
		else
			SendChatMessage(output,"PARTY",jx_commlang,"");
		end
	end

	-- Broadcast stats to raid
	if( (msg=="raid") or (msg=="r") ) then
		-- Verify they are in a raid, yes it is redundancy checking I said that already!
		if ( (GetNumRaidMembers()==nil) or (GetNumRaidMembers() < 1) ) then
			display(AVGXP_ERR_RAID);
		else
			SendChatMessage(output,"RAID",jx_commlang,"");
		end
	end

	-- Broadcast stats to guild
	if( (msg=="guild") or (msg=="g") ) then
		-- Verify they are in a guild, if you're reading this and didn't expect this, you my friend have serious issues with short term memory
		if ( (GetNumGuildMembers()==nil) or (GetNumGuildMembers() < 1) ) then
			display(AVGXP_ERR_GUILD);
		else
			SendChatMessage(output,"GUILD",jx_commlang,"");
		end
	end

	-- Show AvgXPPlus
	if( (msg=="show") or (msg=="s") ) then
		AvgXP:Show();
	end

	-- Hide AvgXPPlus
	if( (msg=="hide") or (msg=="h") ) then
		AvgXP:Hide();
	end

	-- Unlock AvgXPPlus (Allow "move" for backwards compatibility)
	if( (msg=="unlock") or (msg=="u") or (msg=="move") or (msg=="m") ) then
		display(AVGXP_UNLOCKED);
		AvgXP:SetUserPlaced(1);
		AvgXPLocked=false;
	end

	-- Lock AvgXPPlus
	if (msg=="lock") then
		display(AVGXP_LOCKED_MESSAGE);
		AvgXP:SetUserPlaced(1);
		AvgXPLocked=true;
	end

	-- Dock AvgXPPlus
	if ( (msg=="dock") or (msg=="d") ) then
		display(AVGXP_DOCKED_MESSAGE);
		AvgXP:SetUserPlaced(1);
		AvgXP:ClearAllPoints();
		AvgXP:SetPoint("TOPLEFT", "MainMenuBar", "TOPLEFT", -3, 2);
		--set the width of the bars
		AvgXP:SetWidth(AvgXPWidth);
		EXPBar:SetWidth(AvgXPWidth-6);
		EXPBarb:SetWidth(AvgXPWidth-6);
		--set the height of the bars
		AvgXP:SetHeight(AvgXPHeight);
		EXPBar:SetHeight(AvgXPHeight-8);
		EXPBarb:SetHeight(AvgXPHeight-8);
		--set the alpha of the bars
		AvgXP:SetAlpha(AvgXPAlphaAll);
		EXPBar:SetAlpha(AvgXPAlpha);
		AvgXPLocked=true;
		AvgXP_Update_Text()
	end

	-- Reset AvgXPPlus to center of screen
	if ( (msg=="resetgui") or (msg=="rg")) then
		display(AVGXP_RESET_POS);
		AvgXP:SetUserPlaced(1);
		AvgXP:ClearAllPoints();
		AvgXP:SetPoint("CENTER", "UIParent", "CENTER", 0, 0);
		AvgXPLocked=false;
		--reset the width of the bars
		AvgXPWidth = "1028";
		AvgXP:SetWidth(AvgXPWidth);
		EXPBar:SetWidth(AvgXPWidth-6);
		EXPBarb:SetWidth(AvgXPWidth-6);
		--reset the height of the bars
		AvgXPHeight = "16";
		AvgXP:SetHeight(AvgXPHeight);
		EXPBar:SetHeight(AvgXPHeight-8);
		EXPBarb:SetHeight(AvgXPHeight-8);
		--reset the alpha of the bars
		AvgXPAlpha = "1";
		AvgXPAlphaAll = "1";
		AvgXP:SetAlpha(AvgXPAlphaAll);
		EXPBar:SetAlpha(AvgXPAlpha);
		AvgXP_Update_Text()
	end

-- Change the width of the bar.
if ( strfind(msg,"width") ) then
		local index = strfind(msg, " ");
		if (index) then
			AvgXPWidth = strsub(msg, index+1);
			display(string.format(AVGXP_CHANGE_WIDTH, AvgXPWidth));
			AvgXP:SetWidth(AvgXPWidth);
			EXPBar:SetWidth(AvgXPWidth-6);
			EXPBarb:SetWidth(AvgXPWidth-6);
		else
			msg = "";
			display(string.format(AVGXP_CHANGE_WIDTH, AvgXPWidth));
		end
		AvgXP:SetUserPlaced(1);
		AvgXP_Update_Text()
	end

-- Change the height of the bar.
if ( strfind(msg,"height") ) then
		local index = strfind(msg, " ");
		if (index) then
			AvgXPHeight = strsub(msg, index+1);
			display(string.format(AVGXP_CHANGE_HEIGHT, AvgXPHeight));
			AvgXP:SetHeight(AvgXPHeight);
			EXPBar:SetHeight(AvgXPHeight-8);
			EXPBarb:SetHeight(AvgXPHeight-8);
		else
			msg = "";
			display(string.format(AVGXP_CHANGE_HEIGHT, AvgXPHeight));
		end
		AvgXP:SetUserPlaced(1);
		AvgXP_Update_Text()
	end


-- Change the alpha of ALL of AvgXPPlus
if ( strfind(msg,"aa") ) then
    local index = strfind(msg, " ");
    if (index) then
        AvgXPAlphaAll = strsub(msg, index+1);
        display(string.format(AVGXP_AA, AvgXPAlphaAll));
        AvgXP:SetAlpha(AvgXPAlphaAll);
    else
        msg = "";
        display(string.format(AVGXP_CHANGE_AA, AvgXPAlphaAll));
    end
end

	-- Change the alpha of AvgXPPlus
if ( strfind(msg,"alpha") ) then
    local index = strfind(msg, " ");
		if (index) then
			AvgXPAlpha = strsub(msg, index+1);
			display(string.format(AVGXP_ALPHA, AvgXPAlpha));
			EXPBar:SetAlpha(AvgXPAlpha);
		else
			msg = "";
			display(string.format(AVGXP_CHANGE_ALPHA, AvgXPAlpha));
		end
	end

if (msg=="credits") then
    display(AVGXP_CREDITS);
end

-- End of Nanny
end


-- AvgXP_OnEvent()
-- no parms
-- Desc: Event that updates the calculation
function AvgXP_OnEvent(event)
	-- vars: string initialization
	local jx_last_xp_str = "";
	local jx_isrested = false;
	local update = false;

-- Sanity Checks : These are the things that drive you nuts because it makes the script appear like its not working properly when it should be
-- Wait for the player to enter the world before loading (otherwise UnitName(AVGXP_PLAYER) == Nil)
	if(event=="PLAYER_ENTERING_WORLD" and not is_loaded) then

		-- Report Information so we can verify that initialize was called and to show version
		display(string.format(AVGXP_WELCOME, jx_avgexp_version));
		debug(9,string.format("%sDebug is Enabled. Current Threshold is Level %d\n",GRN,jx_debug_threshold));
		debug(1,"Variables loaded, safe to load player and init\n");
		update = true;
		is_loaded = true;
		_ui_loaded = true;

		-- Add /avgxp command line
		SlashCmdList["JXAVGXP"] = JX_AvgXP_Command;
		SLASH_JXAVGXP1 = "/averagexp";
		SLASH_JXAVGXP2 = "/avgxp";

		JX_AvgXP_LoadPlayer();

		-- Setup Best Average
		JX_AvgXP_FindBestAvg();

		--Put the bar to it's last state (locked or unlocked)
		if(AvgXPLocked) then
			AvgXP_Lock();
		else
			AvgXP_Unlock();
		end
	end

	-- If Variables are not loaded do not process anything else
	if (not is_loaded) then
		return;
	end

	-- If something failed with LoadPlayer or its an event call prior to VARIABLES_LOADED do not process anything further
	if(not pid) then
		debug(1,"Breaking because pid is nil\n");
		return;
	end

	-- Check: If not initialized, and we somehow got here, Initialize!
	-- Return to break out, as Initialize will recurse and call Calc again
	-- Note: Found this can occur because PLAYER_XP_UPDATE is called OnLoad I believe
	if( (jx_avgexp_init==nil) or (jx_avgexp_init==false) ) then
		JX_AvgXP_Initialize();
		return;
	end

-- End of Sanity Checks

-- Begin Events

	-- Event: Update display for Target info

	if(event=="PLAYER_TARGET_CHANGED") then
		-- Sanity check
		if (UnitName(AVGXP_TARGET)~=nil) then
			-- If its Focus and not Losing Focus
			-- TODO : change the displayed message based on the width of the bar.
			if (not UnitIsFriend(AVGXP_PLAYER,AVGXP_TARGET)) then
				-- Check if kill tracking exists and display
				if (jx_kill_history[ UnitName(AVGXP_TARGET) ] ~= nil) then
					local stats1 = string.format("%s", UnitName(AVGXP_TARGET));
					local stats2 = string.format(AVGXP_AVGXP_PER_KILL, (jx_kill_history[ UnitName(AVGXP_TARGET) ].total_xp / jx_kill_history[ UnitName(AVGXP_TARGET) ].total_kills));
					local stats3 = string.format(AVGXP_TOTAL_KILLS, jx_kill_history[ UnitName(AVGXP_TARGET) ].total_kills);

					--importance of info.
					--avgxp per kill  stats2
					--total kills     stats3
					--name (%s)       stats1

					jx_target_stats = (stats1.." :: "..stats2.." :: "..stats3);
					if (tonumber(AvgXPWidth) < (string.len(jx_target_stats)*5)-10) then
						jx_target_stats = (stats2.." :: "..stats3);
						if (tonumber(AvgXPWidth) < (string.len(jx_target_stats)*5)-10) then
							jx_target_stats = (stats2);
							if (tonumber(AvgXPWidth) < (string.len(jx_target_stats)*5)-10) then
								jx_target_stats = "";
							end
						end
					end

					AvgXPText:SetText(jx_target_stats);

				else
					-- Check Deprecated history
					if (jx_kill_history_dep[ UnitName(AVGXP_TARGET) ] ~= nil) then
						local stats1 = string.format(AVGXP_LEVEL, jx_kill_history_dep[ UnitName(AVGXP_TARGET) ].level);
						local stats2 = string.format("%s", UnitName(AVGXP_TARGET));
						local stats3 = string.format(AVGXP_AVGXP_PER_KILL, (jx_kill_history_dep[ UnitName(AVGXP_TARGET) ].total_xp / jx_kill_history_dep[ UnitName(AVGXP_TARGET) ].total_kills)  );
						local stats4 = string.format(AVGXP_TOTAL_KILLS, jx_kill_history_dep[ UnitName(AVGXP_TARGET) ].total_kills);

						--importance of info.
						--avgxp per kill  stats3
						--total kills     stats4
						--level           stats1
						--name (%s)       stats2

						jx_target_stats = (stats1.." :: "..stats2.." :: "..stats3.." :: "..stats4);
						if (tonumber(AvgXPWidth) < (string.len(jx_target_stats)*5)-10) then
							jx_target_stats = (stats1.." :: "..stats3.." :: "..stats4);
							if (tonumber(AvgXPWidth) < (string.len(jx_target_stats)*5)-10) then
								jx_target_stats = (stats3.." :: "..stats4);
								if (tonumber(AvgXPWidth) < (string.len(jx_target_stats)*5)-10) then
									jx_target_stats = (stats3);
									if (tonumber(AvgXPWidth) < (string.len(jx_target_stats)*5)-10) then
										jx_target_stats = "";
									end
								end
							end
						end

						AvgXPText:SetText(jx_target_stats);
					end
				end
			end
		else
			-- If nil not displaying stats
			jx_target_stats = nil;
			-- Revert UI to standard Stats on losing focus
			AvgXPText:SetText(jx_avgexp_stats);
		end
	end

	-- Event: Self triggered Calc for the purpose of Updating the UI
	if(event=="JX_AVGXP_INIT") then
		update = true;
	end


  -- Event: Experience gain from Combat (Don't count kills that don't result in Experience, would skew Average Exp Gain)
	if(event=="CHAT_MSG_COMBAT_XP_GAIN") then
		jx_rested_bonus = 0;

		-- When you're rested don't count the rested exp into the average, skews it horribly
		-- By removing rested_bonus from jx_last_xp_gained we essentially process the rested_bonus as a standard PLAYER_XP_UPDATE event
		for rested_bonus in string.gmatch(arg1, AVGXP_CHAT_RESTED) do
			jx_isrested = true;
			jx_rested_bonus = tonumber(rested_bonus);
		end

		for mobile_name, xp in string.gmatch(arg1, AVGXP_XP_GAIN) do
			jx_last_xp_gained = tonumber(xp);
			if (jx_isrested) then
				jx_last_xp_gained = jx_last_xp_gained - jx_rested_bonus;
				msg = string.format("Was Rested %d counted toward average (%d rested)",jx_last_xp_gained, jx_rested_bonus);
				debug(2,msg);
			end

			jx_total_xp_gained = jx_total_xp_gained + jx_last_xp_gained;
			jx_total_mobs_killed = jx_total_mobs_killed + 1;

			-- Tally the Kill (does not include rested bonus)
			JX_AvgXP_AddKill(mobile_name,jx_last_xp_gained);
		end

		-- Update the UI and Save
		update = true;
	end

  -- Event: Gain exp from a quest or exploration, update the UI and Save
	if(event=="PLAYER_XP_UPDATE") then
		AvgXPRate_OnXPUpdate();
		update = true;
	end

	-- Event: Player Level Up, reset stats so we get Average Exp Gain across the entire level
	if(event=="PLAYER_LEVEL_UP") then
		-- Run Level Up to store Level and Reset Avgs
		AvgXPRate_OnLevelUp();
		JX_Event_Player_Level_Up();

		-- Update the UI and Save
		update = true;
	end

	-- If set variables have changed and should update the data, otherwise it fires everytime an event occurs
	if( update == true or event == "PLAYER_ENTERING_WORLD") then
		debug(1,string.format("Update is True - Event that fired is: %s\n",event));
		AvgXP_Update_Text();
	end
	if(UnitLevel(AVGXP_PLAYER)==jx_levelcap)then
		AvgXP:Hide();
	end
	if(UnitLevel(AVGXP_PLAYER)==jx_levelcap)then
		AvgXP:Hide();
	end
-- End Events
end


function AvgXP_Update_Text()
	if(UnitLevel(AVGXP_PLAYER)==jx_levelcap)then
		AvgXP:Hide();
	end
	-- Calculate average (Exp Gained / Kills) - Check for Div by 0 (though LUA doesn't complain)
	if( (jx_total_mobs_killed > 0) and (jx_total_mobs_killed~=nil) ) then
		jx_avg_xp_per_mob = jx_total_xp_gained / jx_total_mobs_killed;
	end

	-- Calculate approximate kills to level - Check for Div by 0 again
	if( (jx_avg_xp_per_mob > 0) and (jx_avg_xp_per_mob~=nil) ) then
		jx_num_mobs_left = ( UnitXPMax(AVGXP_PLAYER) - UnitXP(AVGXP_PLAYER) ) / jx_avg_xp_per_mob;
		jx_num_mobs_left = math.ceil(jx_num_mobs_left); -- Round up
	end

    -- As of Lua 5.1, there is still no ternary operator (i.e. jx_num_mobs_left==1 ? "kill" : "kills")
	if (jx_num_mobs_left == 1) then
		jx_kills_str = AVGXP_KILL;
	else
		jx_kills_str = AVGXP_KILLS;
	end

	-- If last exp gained was different than average, show + for higher, - for lower
	if (jx_last_xp_gained < jx_avg_xp_per_mob) then
		jx_last_xp_str = "- ";
	else
		if (jx_last_xp_gained > jx_avg_xp_per_mob) then
			jx_last_xp_str = "+ ";
		else
			jx_last_xp_str = "  ";
		end
	end

	jx_exp_togo = UnitXPMax(AVGXP_PLAYER) - UnitXP(AVGXP_PLAYER);
	jx_exp_pc = UnitXP(AVGXP_PLAYER) / UnitXPMax(AVGXP_PLAYER) * 100;

	-- Check: Cannot divide by 0, display N/A instead of attempting to display numeric, will result in -1.#IND
	if( (jx_total_mobs_killed==0) or (jx_total_mobs_killed==nil) ) then
		jx_avg_xp_per_mob_str = AVGXP_NA;
	else
		jx_avg_xp_per_mob_str = string.format("%.1f",jx_avg_xp_per_mob);
	end

	-- Setup stats output string
	-- Potential ToDo: Allow for custom string and .gsub to fill with values, possibly with tracking more this would be useful
	local RXP = GetXPExhaustion();
	local stats1;
	if (RXP == nil) then
		stats1 = string.format("%d/%d", UnitXP(AVGXP_PLAYER), UnitXPMax(AVGXP_PLAYER));
	else
		stats1 = string.format("%d/%d (%d)", UnitXP(AVGXP_PLAYER), UnitXPMax(AVGXP_PLAYER), GetXPExhaustion());
	end
	local stats2 = string.format(AVGXP_THRU_LVL, jx_exp_pc, (UnitLevel(AVGXP_PLAYER)+0));
	local stats3 = string.format(AVGXP_XP_LEFT, comma(jx_exp_togo));
	local stats4 = string.format(AVGXP_XP_PER_KILL_S, jx_avg_xp_per_mob_str, jx_last_xp_str);
	local stats5 = string.format(AVGXP_TO_LEVEL, jx_num_mobs_left, jx_kills_str);

	--importance of info.
	--xp/rested xp.     stats1
	--kills to lvl      stats5
	--exp left:         stats3
	--avg xp per kill   stats4
	--% through level   stats2

	jx_avgexp_stats = (stats1.." :: "..stats2.." :: "..stats3.." :: "..stats4..":: "..stats5);
	if (tonumber(AvgXPWidth) < (string.len(jx_avgexp_stats)*5)-10) then
		jx_avgexp_stats = (stats1.." :: "..stats3.." :: "..stats4..":: "..stats5);
		if (tonumber(AvgXPWidth) < (string.len(jx_avgexp_stats)*5)-10) then
			jx_avgexp_stats = (stats1.." :: "..stats3.." :: "..stats5);
			if (tonumber(AvgXPWidth) < (string.len(jx_avgexp_stats)*5)-10) then
				jx_avgexp_stats = (stats1.." :: "..stats5);
				if (tonumber(AvgXPWidth) < (string.len(jx_avgexp_stats)*5)-10) then
					jx_avgexp_stats = (stats1);
					if (tonumber(AvgXPWidth) < (string.len(jx_avgexp_stats)*5)-10) then
						jx_avgexp_stats = "";
					end
				end
			end
		end
	end

	-- Update UI
	AvgXPText:SetText(jx_avgexp_stats);

	-- Update the Saved Table
	JX_AvgXP_SavePlayer();
end
